Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions desktop/ui/src/generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1840,6 +1840,30 @@ export interface paths {
patch?: never;
trace?: never;
};
"/v1/scopes/{scope_id}/artifact-handles/{family}/{key}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* Resolve a stable Artifact handle
* @description Resolve one scope-local handle to the exact immutable Artifact revision it names.
*/
get: operations["resolve_artifact_handle"];
/**
* Bind a stable Artifact handle
* @description Idempotently and permanently bind one scope-local handle to an exact immutable Artifact revision. A committed binding cannot be updated, released, or deleted. Repeating the same binding succeeds, while binding the key to another Artifact revision returns 409. Verify the family, Artifact ID, revision, and key before committing the binding; a mistakenly bound key cannot be reused in that Scope.
*/
put: operations["bind_artifact_handle"];
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/v1/scopes/{scope_id}/artifacts/{family}/{artifact_id}": {
parameters: {
query?: never;
Expand Down Expand Up @@ -2478,6 +2502,15 @@ export interface components {
artifact_id: string;
revision: number;
};
BindArtifactHandleRequest: {
artifact_id: string;
revision: number;
};
ArtifactHandleBinding: {
family: string;
key: string;
artifact: components["schemas"]["ArtifactReference"];
};
ArtifactAddress: {
scope_id: string;
artifact: components["schemas"]["ArtifactReference"];
Expand Down Expand Up @@ -8116,6 +8149,71 @@ export interface operations {
503: components["responses"]["Unavailable"];
};
};
resolve_artifact_handle: {
parameters: {
query?: never;
header?: never;
path: {
scope_id: string;
family: string;
key: string;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description The exact Artifact revision bound to the handle. */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ArtifactHandleBinding"];
};
};
401: components["responses"]["Unauthorized"];
403: components["responses"]["Forbidden"];
404: components["responses"]["NotFound"];
422: components["responses"]["InvalidRequest"];
500: components["responses"]["InternalError"];
503: components["responses"]["Unavailable"];
};
};
bind_artifact_handle: {
parameters: {
query?: never;
header?: never;
path: {
scope_id: string;
family: string;
key: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["BindArtifactHandleRequest"];
};
};
responses: {
/** @description The existing or newly committed handle binding. */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ArtifactHandleBinding"];
};
};
401: components["responses"]["Unauthorized"];
403: components["responses"]["Forbidden"];
404: components["responses"]["NotFound"];
409: components["responses"]["Conflict"];
422: components["responses"]["InvalidRequest"];
500: components["responses"]["InternalError"];
503: components["responses"]["Unavailable"];
};
};
get_artifact: {
parameters: {
query?: never;
Expand Down
2 changes: 1 addition & 1 deletion docs/en/docs/operate/troubleshoot.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ so the previous database remains available for recovery:

```bash
obloader <connection-options> -D <new-database> --csv \
--table 'pc_dream_runs,pc_context_bootstrap_receipts,pc_scope_context_references,pc_scope_external_references,pc_scope_creation_requests,pc_scope_settings,pc_scope_bindings,pc_artifact_heads,pc_artifact_lineage_sources,pc_artifact_lineage_artifacts,pc_artifact_publications,pc_artifact_candidate_versions,pc_topic_memory_revision_publications,pc_memory_entry_versions' \
--table 'pc_dream_runs,pc_context_bootstrap_receipts,pc_scope_context_references,pc_scope_external_references,pc_scope_creation_requests,pc_scope_settings,pc_scope_bindings,pc_artifact_handles,pc_artifact_heads,pc_artifact_lineage_sources,pc_artifact_lineage_artifacts,pc_artifact_publications,pc_artifact_candidate_versions,pc_topic_memory_revision_publications,pc_memory_entry_versions' \
-f <export-directory>
```

Expand Down
9 changes: 9 additions & 0 deletions docs/en/docs/workflows/artifacts.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ Use the scoped HTTP routes in the [API contract](../develop/http-api.md):
Keep the family, Artifact ID, and exact Revision when citing a result or handing it to another Agent.
The current head can advance while a historical Revision remains unchanged.

## Name an exact revision with a stable handle

A stable handle gives one exact Artifact Revision a human-readable, Scope-local name. Bind it with
`PUT /v1/scopes/{scope_id}/artifact-handles/{family}/{key}` and resolve it with `GET` on the same route.

Bindings are permanent. After `(scope_id, family, key)` is committed, it cannot be updated, released, or deleted.
Repeating the same binding is idempotent; binding the key to another Artifact Revision returns `409 Conflict`.
Verify the family, Artifact ID, Revision, and key before binding. If a key is bound by mistake, choose a new key.

Artifact `sources` preserve each source's `source_type` and `source_id`, including dynamically registered types such
as `note`. Treat `source_type` as an open Source name when reading lists, details, and historical revisions.
Source IDs retain accepted Unicode characters and interior spaces, such as `用户偏好` and `release notes`.
Expand Down
2 changes: 1 addition & 1 deletion docs/zh/docs/operate/troubleshoot.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,7 @@ collation,但不会包含数据库 URL 或凭据。

```bash
obloader <connection-options> -D <new-database> --csv \
--table 'pc_dream_runs,pc_context_bootstrap_receipts,pc_scope_context_references,pc_scope_external_references,pc_scope_creation_requests,pc_scope_settings,pc_scope_bindings,pc_artifact_heads,pc_artifact_lineage_sources,pc_artifact_lineage_artifacts,pc_artifact_publications,pc_artifact_candidate_versions,pc_topic_memory_revision_publications,pc_memory_entry_versions' \
--table 'pc_dream_runs,pc_context_bootstrap_receipts,pc_scope_context_references,pc_scope_external_references,pc_scope_creation_requests,pc_scope_settings,pc_scope_bindings,pc_artifact_handles,pc_artifact_heads,pc_artifact_lineage_sources,pc_artifact_lineage_artifacts,pc_artifact_publications,pc_artifact_candidate_versions,pc_topic_memory_revision_publications,pc_memory_entry_versions' \
-f <export-directory>
```

Expand Down
9 changes: 9 additions & 0 deletions docs/zh/docs/workflows/artifacts.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ Replace。缺少 `If-Match` 返回 `428`,ETag 过期返回 `412`;接口不
引用结果或向另一个 Agent 交付时,保留家族、Artifact ID 和精确 Revision。
当前 head 可以前进,历史 Revision 保持不变。

## 使用 Stable Handle 命名精确 Revision

Stable Handle 为一个精确的 Artifact Revision 提供人类可读、Scope 内有效的名称。通过
`PUT /v1/scopes/{scope_id}/artifact-handles/{family}/{key}` 创建绑定,并通过同一路由的 `GET` 解析绑定。

绑定是永久的。`(scope_id, family, key)` 一旦提交,就不能更新、释放或删除。重复提交完全相同的绑定是幂等操作;
将同一个 key 绑定到另一个 Artifact Revision 会返回 `409 Conflict`。绑定前应确认 family、Artifact ID、Revision
和 key;如果错误绑定了某个 key,应改用新的 key。

Artifact 的 `sources` 会保留每个来源的 `source_type` 和 `source_id`,包括 `note` 等动态注册的类型。
读取列表、详情和历史版本时,应将 `source_type` 作为开放的 Source 名称处理。
Source ID 会保留采集时接受的 Unicode 字符和内部空格,例如 `用户偏好` 和 `release notes`。
Expand Down
31 changes: 31 additions & 0 deletions integrations/dsh/plugins/powercontext/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1271,6 +1271,36 @@ const OPERATIONS$1 = {
successStatuses: [200],
emptyStatuses: []
},
resolve_artifact_handle: {
method: "GET",
path: "/v1/scopes/{scope_id}/artifact-handles/{family}/{key}",
location: null,
scopeMode: "none",
pathParameters: [
"scope_id",
"family",
"key"
],
queryParams: [],
headerParams: [],
successStatuses: [200],
emptyStatuses: []
},
bind_artifact_handle: {
method: "PUT",
path: "/v1/scopes/{scope_id}/artifact-handles/{family}/{key}",
location: "body",
scopeMode: "none",
pathParameters: [
"scope_id",
"family",
"key"
],
queryParams: [],
headerParams: [],
successStatuses: [200],
emptyStatuses: []
},
get_artifact: {
method: "GET",
path: "/v1/scopes/{scope_id}/artifacts/{family}/{artifact_id}",
Expand Down Expand Up @@ -1905,6 +1935,7 @@ const PUBLIC_ERROR_CODES = new Set([
"scope_not_found",
"memory_not_found",
"artifact_not_found",
"artifact_handle_not_found",
"candidate_not_found",
"handoff_evidence_not_found",
"source_definition_not_found",
Expand Down
2 changes: 1 addition & 1 deletion integrations/dsh/plugins/powercontext/src/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const COMPATIBILITY_OR_AVAILABILITY_PATHS = new Set([

// Only public protocol codes may cross the diagnostic/model boundary.
const PUBLIC_ERROR_CODES = new Set([
'not_found', 'scope_not_found', 'memory_not_found', 'artifact_not_found',
'not_found', 'scope_not_found', 'memory_not_found', 'artifact_not_found', 'artifact_handle_not_found',
'candidate_not_found', 'handoff_evidence_not_found', 'source_definition_not_found',
'external_skill_not_found', 'conflict', 'revision_conflict', 'memory_entry_inactive',
'memory_capacity_exceeded', 'tag_precondition_failed',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ export const OPERATIONS = {
get_source: { method: 'GET', path: '/v1/scopes/{scope_id}/sources/{source_type}/{source_id}', location: null, scopeMode: 'none', pathParameters: ['scope_id', 'source_type', 'source_id'], queryParams: [], headerParams: [], successStatuses: [200], emptyStatuses: [] },
create_artifact: { method: 'POST', path: '/v1/scopes/{scope_id}/artifacts', location: "body", scopeMode: 'none', pathParameters: ['scope_id'], queryParams: [], headerParams: [], successStatuses: [201], emptyStatuses: [] },
list_artifacts: { method: 'GET', path: '/v1/scopes/{scope_id}/artifacts/{family}', location: "query", scopeMode: 'none', pathParameters: ['scope_id', 'family'], queryParams: ['tag','tag_match','limit','cursor'], headerParams: [], successStatuses: [200], emptyStatuses: [] },
resolve_artifact_handle: { method: 'GET', path: '/v1/scopes/{scope_id}/artifact-handles/{family}/{key}', location: null, scopeMode: 'none', pathParameters: ['scope_id', 'family', 'key'], queryParams: [], headerParams: [], successStatuses: [200], emptyStatuses: [] },
bind_artifact_handle: { method: 'PUT', path: '/v1/scopes/{scope_id}/artifact-handles/{family}/{key}', location: "body", scopeMode: 'none', pathParameters: ['scope_id', 'family', 'key'], queryParams: [], headerParams: [], successStatuses: [200], emptyStatuses: [] },
Comment thread
Tech-Circle-48 marked this conversation as resolved.
get_artifact: { method: 'GET', path: '/v1/scopes/{scope_id}/artifacts/{family}/{artifact_id}', location: null, scopeMode: 'current', pathParameters: ['scope_id', 'family', 'artifact_id'], queryParams: [], headerParams: ['If-None-Match'], successStatuses: [200,304], emptyStatuses: [304] },
replace_artifact: { method: 'PUT', path: '/v1/scopes/{scope_id}/artifacts/{family}/{artifact_id}', location: "body", scopeMode: 'current', pathParameters: ['scope_id', 'family', 'artifact_id'], queryParams: [], headerParams: ['If-Match'], successStatuses: [200], emptyStatuses: [] },
get_artifact_tags: { method: 'GET', path: '/v1/scopes/{scope_id}/artifacts/{family}/{artifact_id}/tags', location: null, scopeMode: 'none', pathParameters: ['scope_id', 'family', 'artifact_id'], queryParams: [], headerParams: ['If-None-Match'], successStatuses: [200,304], emptyStatuses: [304] },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ describe('host-visible diagnostic classification', () => {
describe('published error code filter', () => {
it('passes a published protocol code through unchanged', () => {
expect(publicErrorCode('scope_not_found')).toBe('scope_not_found')
expect(publicErrorCode('artifact_handle_not_found')).toBe('artifact_handle_not_found')

// Both codes the server returns with HTTP 412, and the one it returns with 409
// when a Memory is at its capacity budget, reach the caller as themselves.
expect(publicErrorCode('tag_precondition_failed')).toBe('tag_precondition_failed')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ export const OPERATIONS = {
get_source: { method: 'GET', path: '/v1/scopes/{scope_id}/sources/{source_type}/{source_id}', location: null, scopeMode: 'none', pathParameters: ['scope_id', 'source_type', 'source_id'], queryParams: [], headerParams: [], successStatuses: [200], emptyStatuses: [] },
create_artifact: { method: 'POST', path: '/v1/scopes/{scope_id}/artifacts', location: "body", scopeMode: 'none', pathParameters: ['scope_id'], queryParams: [], headerParams: [], successStatuses: [201], emptyStatuses: [] },
list_artifacts: { method: 'GET', path: '/v1/scopes/{scope_id}/artifacts/{family}', location: "query", scopeMode: 'none', pathParameters: ['scope_id', 'family'], queryParams: ['tag','tag_match','limit','cursor'], headerParams: [], successStatuses: [200], emptyStatuses: [] },
resolve_artifact_handle: { method: 'GET', path: '/v1/scopes/{scope_id}/artifact-handles/{family}/{key}', location: null, scopeMode: 'none', pathParameters: ['scope_id', 'family', 'key'], queryParams: [], headerParams: [], successStatuses: [200], emptyStatuses: [] },
bind_artifact_handle: { method: 'PUT', path: '/v1/scopes/{scope_id}/artifact-handles/{family}/{key}', location: "body", scopeMode: 'none', pathParameters: ['scope_id', 'family', 'key'], queryParams: [], headerParams: [], successStatuses: [200], emptyStatuses: [] },
get_artifact: { method: 'GET', path: '/v1/scopes/{scope_id}/artifacts/{family}/{artifact_id}', location: null, scopeMode: 'current', pathParameters: ['scope_id', 'family', 'artifact_id'], queryParams: [], headerParams: ['If-None-Match'], successStatuses: [200,304], emptyStatuses: [304] },
replace_artifact: { method: 'PUT', path: '/v1/scopes/{scope_id}/artifacts/{family}/{artifact_id}', location: "body", scopeMode: 'current', pathParameters: ['scope_id', 'family', 'artifact_id'], queryParams: [], headerParams: ['If-Match'], successStatuses: [200], emptyStatuses: [] },
get_artifact_tags: { method: 'GET', path: '/v1/scopes/{scope_id}/artifacts/{family}/{artifact_id}/tags', location: null, scopeMode: 'none', pathParameters: ['scope_id', 'family', 'artifact_id'], queryParams: [], headerParams: ['If-None-Match'], successStatuses: [200,304], emptyStatuses: [304] },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ export const OPERATIONS = {
get_source: { method: 'GET', path: '/v1/scopes/{scope_id}/sources/{source_type}/{source_id}', location: null, scopeMode: 'none', pathParameters: ['scope_id', 'source_type', 'source_id'], queryParams: [], headerParams: [], successStatuses: [200], emptyStatuses: [] },
create_artifact: { method: 'POST', path: '/v1/scopes/{scope_id}/artifacts', location: "body", scopeMode: 'none', pathParameters: ['scope_id'], queryParams: [], headerParams: [], successStatuses: [201], emptyStatuses: [] },
list_artifacts: { method: 'GET', path: '/v1/scopes/{scope_id}/artifacts/{family}', location: "query", scopeMode: 'none', pathParameters: ['scope_id', 'family'], queryParams: ['tag','tag_match','limit','cursor'], headerParams: [], successStatuses: [200], emptyStatuses: [] },
resolve_artifact_handle: { method: 'GET', path: '/v1/scopes/{scope_id}/artifact-handles/{family}/{key}', location: null, scopeMode: 'none', pathParameters: ['scope_id', 'family', 'key'], queryParams: [], headerParams: [], successStatuses: [200], emptyStatuses: [] },
bind_artifact_handle: { method: 'PUT', path: '/v1/scopes/{scope_id}/artifact-handles/{family}/{key}', location: "body", scopeMode: 'none', pathParameters: ['scope_id', 'family', 'key'], queryParams: [], headerParams: [], successStatuses: [200], emptyStatuses: [] },
get_artifact: { method: 'GET', path: '/v1/scopes/{scope_id}/artifacts/{family}/{artifact_id}', location: null, scopeMode: 'current', pathParameters: ['scope_id', 'family', 'artifact_id'], queryParams: [], headerParams: ['If-None-Match'], successStatuses: [200,304], emptyStatuses: [304] },
replace_artifact: { method: 'PUT', path: '/v1/scopes/{scope_id}/artifacts/{family}/{artifact_id}', location: "body", scopeMode: 'current', pathParameters: ['scope_id', 'family', 'artifact_id'], queryParams: [], headerParams: ['If-Match'], successStatuses: [200], emptyStatuses: [] },
get_artifact_tags: { method: 'GET', path: '/v1/scopes/{scope_id}/artifacts/{family}/{artifact_id}/tags', location: null, scopeMode: 'none', pathParameters: ['scope_id', 'family', 'artifact_id'], queryParams: [], headerParams: ['If-None-Match'], successStatuses: [200,304], emptyStatuses: [304] },
Expand Down
Loading
Loading