Skip to content

fix(memory-core): expose extraction configuration in skill archive responses - #1358

Open
VittorioCai wants to merge 2 commits into
TencentCloud:feat/server_teamfrom
VittorioCai:fix/skill-extraction-flag
Open

VittorioCai wants to merge 2 commits into
TencentCloud:feat/server_teamfrom
VittorioCai:fix/skill-extraction-flag

Conversation

@VittorioCai

@VittorioCai VittorioCai commented Sep 12, 2026

Copy link
Copy Markdown

Description | 描述

POST /v3/skill/extract, POST /v3/skill/conversation/add and
POST /v3/skill/conversation/force-archive answer a success with ok/status and a
task_id. That answer is accurate: SkillTriggerService.archive() writes the archive,
appends the SkillTaskEntry and calls enqueueAgent, all three inside the tasks mutex.
It has never meant that a Skill was produced — and with skill.extraction.enabled=false
the accepted task cannot be executed in that configuration, while no field in the
response says so.

The caller therefore cannot distinguish "accepted, extraction under way" from "accepted,
extraction cannot run here": both are code: 0 with a task_id. A client that waits for
a skill to appear waits forever, and a client polling /v3/skill/list cannot tell an
empty result caused by the switch from one caused by a session with nothing worth
keeping.

One of those callers is the product's own agent-facing tool. MemoryProxy injects
skill_extract into the read-only tool set of every session
(MemoryProxy/src/injection/injectors/skill-tools-injector.ts), described to the model as
"立即归档当前对话触发一次 skill 抽取(异步任务,由后台 agent 分析对话内容生成 skill)".
When the model calls it, the bridge forwards it to Core's
/v3/skill/conversation/force-archive (skill-bridge.ts, the sub === "extract" branch)
and returns Core's envelope to the model verbatim — the response body is passed through
unchanged except for team-search filtering. So with extraction off, the model is told it
triggered extraction, is handed a task_id, and has nothing in the answer to tell it
otherwise. With this field it does.

This adds one field, extraction_enabled, to the success responses of those three entry
points. It reports the configuration switch and nothing more:

  • not worker readiness and not task completion — true only means the switch is on;
  • null, never false, when the resolved skill config cannot be reached, so
    "cannot tell" stays distinguishable from "it is off".

The archive contract is unchanged: the slice is still archived, the task is still
registered and enqueued, and error responses keep their existing shape. Rejecting the
call instead would be worse — the archive and the task entry are already written by the
time the handler returns, so a failure response would invite a retry that duplicates
them.

Related Issue | 关联 Issue

Related: #972skill_extract is injected unconditionally while the call cannot do what
its description promises. The 40003 "trigger path retired" half of that report no longer
applies (the bridge now forwards extract to conversation/force-archive), but the other
half does: the tool is still advertised to every session, and when extraction is off the
call now succeeds and still produces nothing. This PR does not fix #972 — it does not
change what is injected into the tool set, which is what that issue asks for. It makes the
failure visible in the response instead of silent.

Also related but not overlapping: #1117 (see Additional Notes). No issue was filed for this
specific gap — it was found while running the product end to end and hitting it.

Change Type | 修改类型

  • Bug fix | Bug 修复
  • New feature | 新功能
  • Documentation update | 文档更新 — API doc and both SDKs declare the new field
  • Code optimization | 代码优化

Self-test Checklist | 自测清单

  • Verified locally | 本地验证通过

    npx vitest run src/gateway/skill-archive-extraction-flag.test.ts — 6 tests, all passing:

    1. extraction off — the slice is still archived, archive is still called once,
      ok/task_id/archive_key unchanged, and extraction_enabled: false;
    2. extraction on — response keys are exactly the previous four plus the new one, and
      nothing in it speaks about the outcome of extraction;
    3. config unreachable — null in both cases: the optional dep absent, and the dep
      present but returning undefined (skill not yet constructed). Never a fabricated
      false;
    4. conversation/add — reported both below the threshold (status: "ok") and at it
      (status: "archived", archived preserved);
    5. force-archive — reported on both the empty and the archived answer;
    6. a failed archive keeps its original error (50001, original message) and the new
      field is not injected into the error envelope.

    Also reproduced live against a running Core — evidence under Additional Notes.

  • No existing features affected | 无影响现有功能

    The field is additive on success envelopes only; no request schema, no status code and
    no existing field changes. bash scripts/ci/check-skill-queue-isolation.sh with
    BASE_REF=origin/feat/server_team → PASS. tsc on the touched file reports the same
    18 pre-existing diagnostics before and after (none added). npm pack --dry-run does
    not include the new test file (!src/**/*.test.ts in files), so the published
    package is unchanged in shape. MemoryProxy and MemoryPanel pass the response through
    and ignore unknown fields, so they keep working untouched.

Additional Notes | 其他说明

Reproduction

Live, on standalone + sqlite, with skill.extraction.enabled: false:

curl -sS -H "authorization: Bearer $KEY" -H "x-tdai-user-key: $KEY" \
  -H 'content-type: application/json' -H 'x-tdai-service-id: default' \
  -X POST http://localhost:8420/v3/skill/extract -d '{
    "user_id":"usr-…","team_id":"team-…","agent_id":"agt-repro-extraction-flag",
    "session_id":"repro-1",
    "messages":[{"role":"user","content":"…"},{"role":"assistant","content":"…"}]}'

① the response — no mention of the switch:

{"code":0,"message":"ok","request_id":"req-6f8365f3512d416d",
 "data":{"ok":true,"task_id":"skill-extract-task-fe84b9ed",
         "archived_at_ms":1789235870833,"archive_key":"skill_buffer/…/data-….jsonl"}}

② the task really is registered and enqueued —
…/agt-repro-extraction-flag/_tasks.json after the call:

{"tasks":[{"task_id":"skill-extract-task-fe84b9ed","enqueued_at_ms":1789235870833,
           "archive_key":"skill_buffer/…/data-1789235870833.jsonl", "…":""}]}

③ the worker pool starts and then cannot execute it — docker logs:

INFO  [skill-worker-pool] start pool_id=skill-pool-default-7 concurrency=60 …
ERROR [skill-worker-pool] skill-pool-default-7#0 consumeAgent error:
      [skill-worker-pool] standalone SkillExtractor unavailable (instance=default)
ERROR [skill-worker-pool] skill-pool-default-7#1 consumeAgent error: … (repeats)

An empty candidate pool is deliberately not offered as evidence: a real extraction
may also produce no candidates, so on its own it proves nothing.

Scope — stated explicitly

  • Not a claim that the task is never queued. It is queued; ① – ③ above show the task
    entry and the worker attempts.
  • Not a claim that every storage mode honours the switch. The extractor is built per
    storage mode — service mode builds one per instance
    (buildSkillExtractorForInstance), standalone uses the process singleton
    (TdaiCore.getSkillExtractor) — and this reproduction covers the standalone path only.
    The field reports the process-level resolved configuration, which is what
    getResolvedSkillConfig() returns.
  • Not a change to the retry policy for tasks already in the queue. The repeated
    consumeAgent error above is left exactly as it is.
  • Not a readiness probe. With extraction.enabled=true but no LLMRunner the resolver
    records a degradation and keeps enabled: true; this field still reports true, and
    the doc says so. Surfacing degradations would be a different, larger change.

Relationship to #1117

#1117
(fix(skill): expose extraction action outcomes) reports what extraction did
aggregate candidate outcomes in the worker's logs and task-completion metadata — and
touches candidate-outcome.ts and extract-worker.ts. That is the worker side, after
extraction runs. This PR is the synchronous HTTP response, before anything runs, and
touches skill-handlers.ts. No file and no field overlaps. The two are complementary:
#1117 tells you what came of a task that executed, this tells you whether one can
execute at all.

A search of this repository's full pull-request history (not only recent PRs) for
extraction_enabled, skill.extraction.enabled, the three route paths, force-archive,
and skill-handlers / extract-worker / trigger-service found no other PR touching
these response fields. #1216 also edits skill-handlers.ts, but only handleGet — no
hunk overlap.

Size

26 lines in the implementation (skill-handlers.ts: one helper plus four call sites),
125 lines of test, and 61 lines of API-doc and SDK-type declaration —
v3-api-memorycore-doc.md, the three TypeScript response interfaces, and the three
Python client docstrings — because a response field nobody has documented is not
finished.

CI

.github/workflows/pr-ci.yml triggers on pull_request: branches: [main], so it does
not run for a PR targeting feat/server_team. The checks above were run locally instead;
happy to re-run anything you would like to see.

…sponses

The three archive entry points — /v3/skill/extract, /v3/skill/conversation/add
and /v3/skill/conversation/force-archive — answer a success with ok/status and a
task_id. That is accurate: SkillTriggerService.archive() writes the archive,
appends the SkillTaskEntry and calls enqueueAgent, all three inside the tasks
mutex. It has never meant that a Skill was produced, and with
skill.extraction.enabled=false nothing in the response says the accepted task
cannot be executed in this configuration.

Observed on standalone/sqlite: the response is code 0 with a task_id, the task
is registered in the agent's _tasks.json with enqueued_at_ms, the worker pool
starts, and every worker then logs

  [skill-worker-pool] consumeAgent error: standalone SkillExtractor unavailable

and retries. None of that is visible to the caller, which therefore waits.

This adds one field, extraction_enabled, to the success responses of the three
entry points. It reports the configuration switch and nothing more:

  - not worker readiness and not task completion — true only means the switch
    is on;
  - null, never false, when the resolved config cannot be reached, so
    "cannot tell" stays distinguishable from "it is off".

Deliberately out of scope: the retry policy for tasks already in the queue, and
any claim that every storage mode honours the switch — the extractor is built
per storage mode (service builds one per instance, standalone uses the process
singleton). Error responses are unchanged.

Signed-off-by: Vittorio Cai <vittoriocaiyx@gmail.com>
The field is only useful if a caller knows it exists and knows what it does
not mean. The three archive entry points' response shapes in
v3-api-memorycore-doc.md, the TypeScript response interfaces and the Python
client docstrings now all carry it, with the same limits stated in one place:
the configuration switch, not worker readiness and not completion; null is
"unknown", not "off".

MemoryProxy and MemoryPanel pass the response through and ignore unknown
fields, so they keep working untouched; surfacing the switch in those two
surfaces is a separate change.

Signed-off-by: Vittorio Cai <vittoriocaiyx@gmail.com>
VittorioCai pushed a commit to VittorioCai/TencentDB-Agent-Memory that referenced this pull request Sep 12, 2026
…hecks are in the record

Recorded in the three places a reader lands: the PR description's upstream section (now
links TencentCloud#1358 instead of saying "prepared"), STATE's fifth-review section, and the
candidate's own README, which gains the verification table.

Checked after it opened, by comparison rather than by eye: base feat/server_team and
head VittorioCai:fix/skill-extraction-flag; two commits, both authored by
Vittorio Cai <vittoriocaiyx@gmail.com> and both carrying the DCO sign-off; +212/−5 in
the same five files as locally; the body's five headings and six checkboxes are
word-for-word the upstream template (diff of the '## ' lines is empty), with Bug fix and
Documentation update ticked and both self-test lines carrying their command and result;
zero hits for an AI-generated marker.

No automated check runs on it — pr-ci.yml triggers on `pull_request: branches: [main]`
and this PR targets feat/server_team — so the body's self-test is the only evidence.
Noted with it that re-pointing the base at main is not an option (unrelated history), so
a maintainer who wants CI has to trigger it manually or re-run locally.
VittorioCai pushed a commit to VittorioCai/TencentDB-Agent-Memory that referenced this pull request Sep 12, 2026
The teacher's requirement on record (2026-09-07) is "GitHub branch code + documentation",
not a pull request, so closing fork PR #1 costs nothing it was asked to provide. It also
was not a good browsing entry: its diff baseline is upstream feat/server_team, so it
counted the fork's entire delta against upstream — 2596 files, +233680 — rather than this
work.

PR-DESCRIPTION, STATE and REVIEW-GUIDE now name the branch as the deliverable
(tree/topic4-attribution-gate) and say the PR was opened as an entry point and closed on
the user's decision, with the branch and its history unaffected. PR-DESCRIPTION.md stays
the delivery document; it simply stops being a PR body.

Upstream PR TencentCloud#1358 is a separate branch's contribution and stays open.
VittorioCai pushed a commit to VittorioCai/TencentDB-Agent-Memory that referenced this pull request Sep 12, 2026
Looking for whether this PR had an issue to point at turned up something better: the
caller that cannot tell is the model itself. MemoryProxy injects skill_extract into every
session's read-only tool set, describing it as triggering a skill extraction; the bridge's
`sub === "extract"` branch forwards it to Core's /v3/skill/conversation/force-archive —
one of the three endpoints this PR touches — and returns Core's envelope to the model
verbatim (finalRespText = respText, rewritten only for team search). So with extraction
off the model is told it triggered extraction, is handed a task_id, and has nothing in the
answer to say otherwise.

Issue TencentCloud#972 reported that same tool as injected-but-unusable. Its 40003 "trigger path
retired" half no longer applies — the bridge now forwards instead of rejecting — but the
other half does, and is now quieter: the call succeeds and still produces nothing. The
body relates to TencentCloud#972 and states plainly that this PR does NOT fix it, since TencentCloud#972 asks for
the tool to be removed from the injected set, which this does not touch.

Corrects my own claim in the previous exchange that the body had already been edited; it
had not.
VittorioCai pushed a commit to VittorioCai/TencentDB-Agent-Memory that referenced this pull request Sep 13, 2026
TencentCloud#1359 gets its own folder — problem, fix scope, evidence, dedup, remaining defects — and
the PR body is kept beside it so the submitted text is reproducible. TencentCloud#1358's folder gains
the same status wording it was missing.

Both are labelled "已提交上游,待审核" and nothing else. Three things are stated rather than
left to inference: neither is merged; `mergeable=MERGEABLE` means no textual conflict, not
a review verdict; and neither has any automated check at all, because upstream's pr-ci.yml
triggers on `pull_request: branches: [main]` while both PRs target feat/server_team — so
every "通过" in these folders means a local run and is written that way.

evaluation/upstream/README.md indexes the two and records what was found but deliberately
not submitted: one defect whose disclosure rule sends it to private email rather than a
public issue or PR, which the user declined to send, recorded without detail; and the three
other reproduced proxy defects, which wait behind delivery acceptance.

PR-DESCRIPTION, STATE and REVIEW-GUIDE now describe two upstream contributions instead of
one, with the same non-overclaiming wording.
@Maxwell-Code07

Copy link
Copy Markdown
Collaborator

Thank you so much for your attention and contribution! We will arrange an internal review for this PR shortly, and all feedback will be shared right here in the discussion.

VittorioCai pushed a commit to VittorioCai/TencentDB-Agent-Memory that referenced this pull request Sep 13, 2026
… longer matched the branch

Read as a reviewer would, front to back. The worst find is that PR-DESCRIPTION.md —
the delivery description itself — never mentioned the third dev-loop task. Zero hits
for resource-download. Someone reading the document that says what was delivered would
not have known the scenario exists, let alone that its first batch was voided. It is
now item 5, with the three things that have to be said alongside the 0/4 against 4/4:
every use event is needs_review, the working copy carried a shared alternative source
that the two arms read unequally (4/4 against 2/4) so its effect on the gap cannot be
determined, and the first sixteen runs were voided because I had left the reference
implementation on disk. The later items are renumbered.

Two documents claimed deliver-check "regenerates them all" and diffs them. It does not:
thirteen are regenerated, one is registered as not regenerable with its cost, fifteen
are historical. Both sentences now say what actually happens and point at the registry.

Counts that had moved on: six rounds of review became eight (and rounds seven and eight
are now written up in both PR-DESCRIPTION and REVIEW-GUIDE rather than only in STATE);
conditions-check's twenty FAILs became twenty-one, and its post-batch class fourteen to
fifteen; the committed run directories gained the third task's twenty-four; the sample
line said 5+5 and 2+2 and now says 4+4 too; task five's row in the official-task table
said two dev-loop tasks; and the claim that upstream PR bodies had been checked for the
AI marker named only TencentCloud#1358 when there are four.

One duplication removed: the gate-port bullet stated "four conflicts, all of them import
lists" twice after an earlier edit.

Dead-link scan across all four documents: zero. Suite 746/746, acceptance offline and
live both pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] skill_extract 工具被无条件注入,但调用恒返回 40003「触发路径已下线」

2 participants